package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.events.Event;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Component
public class CacheClient {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //封装缓存穿透返回空值的set和get
    public void set(String key, Object value, Long expireTIme, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTIme, unit);
    }

    public <ID, R> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbCallBack, Long expireTIme, TimeUnit unit) {
        //1从redis中查询shop缓存
        String keyStr = keyPrefix + id.toString();
        String cacheData = stringRedisTemplate.opsForValue().get(keyStr);
        //2查询到了，直接返回
        if (StrUtil.isNotBlank(cacheData)) {
            //不为null，不为""，不为换行符等，必须为实际的字符
            R r = JSONUtil.toBean(cacheData, type);
            return r;
        }
        //为空对象，即""的情况
        if (cacheData != null) {
            return null;
        }
        //3没查询到，查询数据库
        R r = dbCallBack.apply(id);

        //4数据库也没查到，返回fail
        if (r == null) {
            //存入空对象
//            stringRedisTemplate.opsForValue().set(keyStr, "", CACHE_NULL_TTL, TimeUnit.SECONDS);
            set(keyStr,"",CACHE_NULL_TTL, TimeUnit.SECONDS);
            return null;
        }
        //5数据库查到了，结果存放在redis中
        set(keyStr,r,expireTIme,unit);
        //6返回结果
        return r;
    }


    //封装缓存击穿使用逻辑过期的set和get
    public void setWithLogicalExpire(String key, Object value, Long expireTIme, TimeUnit unit) {
        RedisData redisData = RedisData.builder()
                .data(value)
                .expireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(expireTIme)))
                .build();
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    //逻辑过期解决缓存穿透问题
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public<R,ID> R queryWithLogicalExpire(String keyPrefix,String lockKeyPrefix, ID id, Class<R> type, Function<ID, R> dbCallBack, Long expireTIme, TimeUnit unit) {
        //1从redis中查询shop缓存
        String keyStr = keyPrefix + id.toString();
        String cacheData = stringRedisTemplate.opsForValue().get(keyStr);
        //2没查到
        //默认只要存在一定能查到，只不过可能过期了而已
        if (StrUtil.isBlank(cacheData)) {
            //为null，不为""，不为换行符等，必须为实际的字符
            return null;
        }
        //3查询到
        //4判断是否过期
        RedisData redisData = JSONUtil.toBean(cacheData, RedisData.class);
        Object data = redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
        JSONObject jsonObject = (JSONObject) data;
        R r = JSONUtil.toBean(jsonObject, type);
        //判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //没过期
            return r;
        }
        //过期
        //获取锁
        String lockKey = lockKeyPrefix + id;
        //获取成功
        boolean flag = tryLock(lockKey);
        if (flag) {
            //开启独立线程
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    R r1 = dbCallBack.apply(id);
                    setWithLogicalExpire(keyStr,r1,expireTIme,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    unLock(lockKey);
                }
            });
        }

        //获取失败
        //返回结果
        return r;
    }

    //使用redis模拟锁
    public boolean tryLock(String key) {
        //尝试set
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //就可能出现包装类的情况
        return BooleanUtil.isTrue(flag);
    }

    public void unLock(String key) {
        //尝试delete
        stringRedisTemplate.delete(key);
        return;
    }
}
